﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Linq.Expressions;
using System.Reflection;
using DotNetRepository.Tools;

namespace DotNetRepository
{
    public abstract class BaseRepository<TEntity> where TEntity : class, IEntity
    {
        protected abstract IEntityRepository<TEntity> Self{ get;}

        protected readonly ValueLock<Guid> GuidLock = new ValueLock<Guid>();
        protected readonly bool HasEntityCollections = EntityReflector<TEntity>.HasEntityCollections;
        protected readonly bool HasEnumerationsToNotify = EntityReflector<TEntity>.EntityRefEnumerationPairs.Count > 0;
        protected readonly bool HasChildEntitiesToPersist = EntityReflector<TEntity>.HasChildEntitiesToPersist;
        protected readonly bool HasChildEntitiesToDelete = EntityReflector<TEntity>.HasChildEntitiesToDelete;
        protected readonly bool HasEntityRefs = EntityReflector<TEntity>.HasEntityRefs;

        public abstract TEntity GetByID(Guid id);

        public abstract TEntity GetCachedByID(Guid id);

        /// <summary>
        /// Returns true if changes have been made to an object that have not yet been saved.
        /// </summary>
        /// <param name="entity"></param>
        /// <returns></returns>
        public abstract bool IsDirty(TEntity entity);

        public void Save(TEntity entity)
        {
            Save(entity, null);
        }

        public abstract void Save(TEntity entity, object sender);

        protected void InitializeEntityCollections(TEntity entity)
        {
            if (HasEntityCollections)
                EntityReflector<TEntity>.InitializeEntityCollections(entity, Self);
        }

        protected void InitializeEntity(TEntity entity)
        {
            InitializeEntityCollections(entity);
        }

        protected void SaveChildEntities(TEntity entity)
        {
            if (!HasChildEntitiesToPersist) return;

            var childEntities = EntityReflector<TEntity>.GetChildEntitiesToPersist(entity);
            foreach (var childEntity in childEntities)
            {
                Save(childEntity);
            }
        }

        protected void SaveCollections(TEntity entity)
        {
            if (!HasEntityCollections) return;

            var collections = EntityReflector<TEntity>.GetEntityCollections(entity,Self);
            foreach (var collection in collections)
            {
                collection.SubmitChanges();
            }
                
        }

        protected void RevertCollections(TEntity entity)
        {
            if (!HasEntityCollections) return;

            var collections = EntityReflector<TEntity>.GetEntityCollections(entity,Self);
            foreach (var collection in collections)
            {
                collection.RevertChanges();
            }

        }

        public void ValidateEnity(TEntity entity)
        {
            foreach (var entityRef in
                EntityReflector<TEntity>.GetEntityRefs(entity).Where(entityRef => !entityRef.AllowNull && entityRef.ID == Guid.Empty))
            {
                throw new ApplicationException(
                    string.Format("Error saving {0}! {1} cannot be null.",
                                  typeof(TEntity).Name, entityRef.GetType().GetGenericArguments().FirstOrDefault().GetName()));
            }
        }

        public abstract void Revert(TEntity entity);

        public void Delete(TEntity entity)
        {
            Delete(entity, null);
        }

        public abstract void Delete(TEntity entity, object sender);

        public void DeleteChildEntities(TEntity entity)
        {
            if (!HasChildEntitiesToDelete) return;
            var childEntities = EntityReflector<TEntity>.GetChildEntitiesToDelete(entity);
            foreach (var childEntity in childEntities)
            {
                Delete(childEntity);
            }
        }

        public void ClearEntityCollections(TEntity entity)
        {
            if (!HasEntityCollections) return;
            var collections = EntityReflector<TEntity>.GetEntityCollections(entity,Self);
            foreach (var collection in collections)
            {
                collection.ClearAndSubmit();
            }
        }

        public abstract bool IsCached(Guid id);

        public abstract bool Contains(TEntity entity);

        public bool Contains(Guid id)
        {
            if (id == Guid.Empty) return false;
            try
            {
                var entity = GetByID(id);
                return entity != null;
            }
            catch (ArgumentOutOfRangeException)
            {
                return false;
            }
        }

        public abstract bool Any(Expression<Func<TEntity, bool>> predicate);

        public abstract List<Guid> GetIDs(Expression<Func<TEntity, bool>> predicate);

        public abstract List<TEntity> Where(Expression<Func<TEntity, bool>> predicate);

        public List<TEntity> GetByIDs(IEnumerable<Guid> idList)
        {
            /*
            return idList.Select(
                id =>
                    {
                        try
                        {
                            return GetByID(id);
                        }
                        catch (ArgumentOutOfRangeException)
                        {
                            return null;
                        }
                    }
                ).Where(e=>e!=null).ToList();
            */
            return Where(e => idList.Contains(e.ID));
        }

        public abstract TEntity FirstOrDefault(Expression<Func<TEntity, bool>> filter);

        public abstract TEntity LastOrDefault(Expression<Func<TEntity, bool>> filter);

        protected static void NotifyEntityDeleted(TEntity entity, object sender)
        {
            foreach (var entityRefField in EntityReflector<TEntity>.EntityRefEnumerationPairs.Keys)
            {
                var entityCollections = GetEntityCollectionsToNotify(entity, entityRefField, true);
                if (entityCollections == null) return;
                foreach (var entityCollection in entityCollections)
                {
                    if (entityCollection != null && entityCollection != sender)
                        entityCollection.OnEntityDeleted(entity.ID);
                }
            }
        }

        protected static void NotifyEntityUpdated(TEntity entity, object sender)
        {
            foreach (var entityRefField in EntityReflector<TEntity>.EntityRefEnumerationPairs.Keys)
            {
                var entityCollections = GetEntityCollectionsToNotify(entity, entityRefField, false);
                if (entityCollections == null) continue;  // unable to retrieve entity collection

                foreach (var entityCollection in entityCollections)
                {
                    if (entityCollection != sender) // only notify if it isn't the one requesting the update
                        entityCollection.OnEntityUpdated(entity);
                }
            }
        }

        private static IEnumerable<IEntityEnumeration> GetEntityCollectionsToNotify(TEntity entity, MemberInfo entityRefMember, bool deleted)
        {
            var entityRef = entityRefMember.GetValue(entity) as IEntityRef;
            if (entityRef == null) return null; // unable to retrieve entity ref}

            var entityIsNew = entity.Version == 1;
            //bool entityIsNew = !Instance.IsCached(entity.ID);

            if (entityRef.ReadOnly && !entityIsNew && !deleted)
                return null; // no need to update since this is readonly and not new, so it can't have changed}

            if (entityIsNew)
            {
                if (entityRef.ID == Guid.Empty || // EntityRef.ID is Empty so no one to notify
                    !entityRef.IsCached)  // EntityRef is not in memory, so it doesn't need to be notified
                    return null;  // 
            }

            var newEntityEnumeration = (entityRef.ID == Guid.Empty || !entityRef.IsCached) ? null :
                EntityReflector<TEntity>.EntityRefEnumerationPairs[entityRefMember].GetValue(entityRef.Value) as IEntityEnumeration;

            if (entityIsNew)
                return newEntityEnumeration == null ? null : new[] { newEntityEnumeration };

            // Not New and Not ReadOnly
            if (entityRef.ID == entityRef.PreviousID)
                return null; // no changes made

            var oldEntityEnumeration = entityRef.PreviousID == Guid.Empty || !entityRef.PreviousValueIsCached ? null :
                EntityReflector<TEntity>.EntityRefEnumerationPairs[entityRefMember].GetValue(entityRef.PreviousValue) as IEntityEnumeration;

            if (oldEntityEnumeration == null && newEntityEnumeration == null) return null;

            if (oldEntityEnumeration == null)
                return new[] { newEntityEnumeration };
            if (newEntityEnumeration == null)
                return new[] { oldEntityEnumeration };

            return new[] { newEntityEnumeration, oldEntityEnumeration };
        }

        //protected abstract Type GetGenericRepositoryType(IEntity entity);

        //protected abstract Type GetGenericRepositoryType<T>() where T : class, IEntity;

        //public object GetGenericRepositoryInstance(IEntity entity)
        //{
        //    var repositoryType = GetGenericRepositoryType(entity); 
        //    var repositoryInstance = repositoryType.GetField("Instance", BindingFlags.Static | BindingFlags.Public).GetValue(null);
        //    return repositoryInstance;
        //}

        //public IEntityRepository<T> GetGenericRepository<T>() where T : class, IEntity
        //{
        //    var repositoryType = GetGenericRepositoryType<T>();
        //    var repositoryInstance = repositoryType.GetField("Instance", BindingFlags.Static | BindingFlags.Public).GetValue(null);
        //    return repositoryInstance as IEntityRepository<T>;
        //}

        private static void Save(IEntity entity)
        {
            Repository.Save(entity, null);
        }

        private static void Delete(IEntity entity)
        {
            Repository.Delete(entity, null);
        }
    }
}
